// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.IO;
using System.Xml;
using Newtonsoft.Json;

namespace SnapObjects.Data
{
    internal class XNodeReader : INodeReader
    {
        private const string STANDARDROOT = "root";

        private readonly int _depth;
        private readonly bool _isEmpty;
        private readonly XmlReader _reader;

        public static INodeReader Parse(string raw)
        {
            return new XNodeReader(raw);
        }

        public static INodeReader Parse(Stream raw)
        {
            return new XNodeReader(raw);
        }

        private XNodeReader(string raw)
        {
            if (String.IsNullOrWhiteSpace(raw))
            {
                throw new XmlException(Properties.Resource.DATAREADER_NOTNULL_EXCEPTION);
            }

            _reader = XmlReader.Create(new StringReader(raw), this.CreateXmlReaderSettings());

            this.InitNode();
        }

        private XNodeReader(Stream raw)
        {
            if (raw == null || raw.Length <= 0)
            {
                throw new XmlException(Properties.Resource.DATAREADER_NOTNULL_EXCEPTION);
            }

            _reader = XmlReader.Create(new StreamReader(raw), this.CreateXmlReaderSettings());

            this.InitNode();
        }

        private XmlReaderSettings CreateXmlReaderSettings()
        {
            var settings = new XmlReaderSettings
            {
                IgnoreComments = true,
                IgnoreWhitespace = true,
                IgnoreProcessingInstructions = true
            };

            return settings;
        }

        private XNodeReader(XmlReader reader, string name, DataNodeType type)
        {
            _depth = reader.Depth;
            _reader = reader;
            _isEmpty = reader.IsEmptyElement;

            this.Name = name;
            this.NodeType = type;
        }

        public bool IsEnd { get; private set; }
        public string Name { get; private set; }
        public string CurrentNodeName { get; private set; }
        public DataNodeType NodeType { get; private set; }
        public DataNodeType CurrentNodeType { get; private set; }

        public IValueReader ValueReader
        {
            get
            {
                this.CheckEnd();

                if (this.CurrentNodeType != DataNodeType.Value)
                {
                    throw new JsonReaderException($"Unexpected token '{this.Name}'.");
                }

                return new XValueReader(this.Name, _reader.Value);
            }
        }

        public INodeReader ReadAsNode()
        {
            this.CheckEnd();

            if (this.CurrentNodeType == DataNodeType.Object
                && _reader.NodeType == XmlNodeType.Element)
            {
                return new XNodeReader(_reader, this.CurrentNodeName, this.CurrentNodeType);
            }
            else
            {
                throw new XmlException($"Unexpected token '{_reader.Name}'.");
            }
        }

        public IValueReader ReadAsRaw()
        {
            var valueReader = new XValueReader(_reader.Name, _reader.ReadInnerXml());

            //Determine whether the object has finished reading
            if (_depth == _reader.Depth)
            {
                this.IsEnd = true;
            }

            return valueReader;
        }

        public bool Read()
        {
            this.CheckEnd();

            if (_isEmpty)
            {
                this.IsEnd = true;

                return false;
            }

            while (_reader.Read())
            {
                if (_reader.NodeType == XmlNodeType.EndElement
                    && _depth == _reader.Depth)
                {
                    this.IsEnd = true;
                    return false;
                }

                if (_reader.NodeType != XmlNodeType.Element
                    && _reader.NodeType != XmlNodeType.Text
                    && _reader.NodeType != XmlNodeType.CDATA)
                {
                    continue;
                }

                this.CurrentNodeName = _reader.Name;

                switch (_reader.NodeType)
                {
                    case XmlNodeType.Element: this.CurrentNodeType = DataNodeType.Object; break;
                    default: this.CurrentNodeType = DataNodeType.Value; break;
                }

                return true;
            }

            this.IsEnd = true;

            return false;
        }

        public void SkipCurrentNode()
        {
            _reader.Skip();

            if (_reader.NodeType == XmlNodeType.EndElement &&
                _depth == _reader.Depth)
            {
                this.IsEnd = true;
            }
        }

        private void InitNode()
        {
            this.IsEnd = true;

            while (_reader.Read())
            {
                if (_reader.NodeType == XmlNodeType.Element)
                {
                    if (_reader.Name.Equals(STANDARDROOT, StringComparison.OrdinalIgnoreCase))
                    {
                        this.NodeType = DataNodeType.Object;
                    }
                    else
                    {
                        this.NodeType = DataNodeType.Array;
                    }

                    this.Name = _reader.Name;
                    this.IsEnd = false;

                    break;
                }
            }

            if (_reader.NodeType != XmlNodeType.Element)
            {
                throw new XmlException($"Unexpected token '{_reader.Name}'.");
            }
        }

        private void CheckEnd()
        {
            if (this.IsEnd)
            {
                throw new XmlException("The node has been read to the end.");
            }
        }
    }
}
